/*******************************************************************************
 * Copyright (c) 2012-2017 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.api.workspace.server.stack;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;

import org.apache.commons.fileupload.FileItem;
import org.eclipse.che.api.core.ApiException;
import org.eclipse.che.api.core.BadRequestException;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.ForbiddenException;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.rest.Service;
import org.eclipse.che.api.core.rest.annotations.GenerateLink;
import org.eclipse.che.api.core.rest.shared.dto.Link;
import org.eclipse.che.api.core.util.LinksHelper;
import org.eclipse.che.api.workspace.server.model.impl.stack.StackImpl;
import org.eclipse.che.api.workspace.server.spi.StackDao;
import org.eclipse.che.api.workspace.server.stack.image.StackIcon;
import org.eclipse.che.api.workspace.shared.dto.stack.StackDto;
import org.eclipse.che.commons.env.EnvironmentContext;

import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.MULTIPART_FORM_DATA;
import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
import static javax.ws.rs.core.Response.Status.CREATED;
import static org.eclipse.che.api.workspace.server.DtoConverter.asDto;
import static org.eclipse.che.api.workspace.shared.Constants.LINK_REL_CREATE_STACK;
import static org.eclipse.che.api.workspace.shared.Constants.LINK_REL_DELETE_ICON;
import static org.eclipse.che.api.workspace.shared.Constants.LINK_REL_GET_ICON;
import static org.eclipse.che.api.workspace.shared.Constants.LINK_REL_GET_STACK_BY_ID;
import static org.eclipse.che.api.workspace.shared.Constants.LINK_REL_REMOVE_STACK;
import static org.eclipse.che.api.workspace.shared.Constants.LINK_REL_SEARCH_STACKS;
import static org.eclipse.che.api.workspace.shared.Constants.LINK_REL_UPDATE_STACK;
import static org.eclipse.che.api.workspace.shared.Constants.LINK_REL_UPLOAD_ICON;

/**
 * Defines Stack REST API
 *
 * @author Alexander Andrienko
 */
@Api(value = "/stack", description = "Stack REST API")
@Path("/stack")
public class StackService extends Service {

    private final StackDao       stackDao;
    private final StackValidator stackValidator;

    @Inject
    public StackService(StackDao stackDao, StackValidator stackValidator) {
        this.stackDao = stackDao;
        this.stackValidator = stackValidator;
    }

    @POST
    @Consumes(APPLICATION_JSON)
    @Produces(APPLICATION_JSON)
    @GenerateLink(rel = LINK_REL_CREATE_STACK)
    @ApiOperation(value = "Create a new stack",
                  notes = "This operation can be performed only by authorized user",
                  response = StackDto.class)
    @ApiResponses({@ApiResponse(code = 201, message = "The stack successfully created"),
                   @ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"),
                   @ApiResponse(code = 403, message = "The user does not have access to create a new stack"),
                   @ApiResponse(code = 409, message = "Conflict error occurred during the stack creation" +
                                                      "(e.g. The stack with such name already exists)"),
                   @ApiResponse(code = 500, message = "Internal server error occurred")})
    public Response createStack(@ApiParam("The new stack") final StackDto stackDto) throws ApiException {
        stackValidator.check(stackDto);
        final String userId = EnvironmentContext.getCurrent().getSubject().getUserId();
        final StackImpl newStack = StackImpl.builder()
                                            .generateId()
                                            .setName(stackDto.getName())
                                            .setDescription(stackDto.getDescription())
                                            .setScope(stackDto.getScope())
                                            .setCreator(userId)
                                            .setTags(stackDto.getTags())
                                            .setWorkspaceConfig(stackDto.getWorkspaceConfig())
                                            .setSource(stackDto.getSource())
                                            .setComponents(stackDto.getComponents())
                                            .build();
        stackDao.create(newStack);

        return Response.status(CREATED)
                       .entity(asStackDto(newStack))
                       .build();
    }

    @GET
    @Path("/{id}")
    @Produces(APPLICATION_JSON)
    @GenerateLink(rel = LINK_REL_GET_STACK_BY_ID)
    @ApiOperation(value = "Get the stack by id",
                  notes = "This operation can be performed for stack owner, or for predefined stacks")
    @ApiResponses({@ApiResponse(code = 200, message = "The response contains requested stack entity"),
                   @ApiResponse(code = 404, message = "The requested stack was not found"),
                   @ApiResponse(code = 403, message = "The user has not permission get requested stack"),
                   @ApiResponse(code = 500, message = "Internal server error occurred")})
    public StackDto getStack(@ApiParam("The stack id") @PathParam("id") final String id) throws ApiException {
        return asStackDto(stackDao.getById(id));
    }

    @PUT
    @Path("/{id}")
    @Produces(APPLICATION_JSON)
    @Consumes(APPLICATION_JSON)
    @GenerateLink(rel = LINK_REL_UPDATE_STACK)
    @ApiOperation(value = "Update the stack by replacing all the existing data (exclude field \"creator\") with update")
    @ApiResponses({@ApiResponse(code = 200, message = "The stack successfully updated"),
                   @ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"),
                   @ApiResponse(code = 403, message = "The user does not have access to update the stack"),
                   @ApiResponse(code = 409, message = "Conflict error occurred during stack update" +
                                                      "(e.g. Stack with such name already exists)"),
                   @ApiResponse(code = 500, message = "Internal server error occurred")})
    public StackDto updateStack(@ApiParam(value = "The stack update", required = true)
                                final StackDto updateDto,
                                @ApiParam(value = "The stack id", required = true)
                                @PathParam("id")
                                final String id) throws ApiException {
        stackValidator.check(updateDto);
        final StackImpl stack = stackDao.getById(id);

        StackImpl stackForUpdate = StackImpl.builder()
                                            .setId(id)
                                            .setName(updateDto.getName())
                                            .setDescription(updateDto.getDescription())
                                            .setScope(updateDto.getScope())
                                            //user can't edit creator
                                            .setCreator(stack.getCreator())
                                            .setTags(updateDto.getTags())
                                            .setWorkspaceConfig(updateDto.getWorkspaceConfig())
                                            .setSource(updateDto.getSource())
                                            .setComponents(updateDto.getComponents())
                                            .build();

        return asStackDto(stackDao.update(stackForUpdate));
    }

    @DELETE
    @Path("/{id}")
    @GenerateLink(rel = LINK_REL_REMOVE_STACK)
    @ApiOperation(value = "Removes the stack")
    @ApiResponses({@ApiResponse(code = 204, message = "The stack successfully removed"),
                   @ApiResponse(code = 403, message = "The user does not have access to remove the stack"),
                   @ApiResponse(code = 404, message = "The stack doesn't exist"),
                   @ApiResponse(code = 500, message = "Internal server error occurred")})
    public void removeStack(@ApiParam("The stack id") @PathParam("id") final String id) throws ApiException {
        stackDao.remove(id);
    }

    @GET
    @Produces(APPLICATION_JSON)
    @GenerateLink(rel = LINK_REL_SEARCH_STACKS)
    @ApiOperation(value = "Get the list stacks with required tags",
                  notes = "This operation can be performed only by authorized user",
                  response = StackDto.class,
                  responseContainer = "List")
    @ApiResponses({@ApiResponse(code = 200, message = "The response contains requested list stack entity with required tags"),
                   @ApiResponse(code = 403, message = "The user does not have access to get stack entity list with required tags"),
                   @ApiResponse(code = 500, message = "Internal server error occurred")})
    public List<StackDto> searchStacks(@ApiParam("List tags for search")
                                       @QueryParam("tags")
                                       final List<String> tags,
                                       @ApiParam(value = "The number of the items to skip")
                                       @DefaultValue("0")
                                       @QueryParam("skipCount")
                                       final Integer skipCount,
                                       @ApiParam("The limit of the items in the response, default is 30")
                                       @DefaultValue("30")
                                       @QueryParam("maxItems")
                                       final Integer maxItems) throws ServerException {
        final String currentUser = EnvironmentContext.getCurrent().getSubject().getUserId();
        return stackDao.searchStacks(currentUser, tags, skipCount, maxItems)
                       .stream()
                       .map(this::asStackDto)
                       .collect(Collectors.toList());
    }

    @GET
    @Path("/{id}/icon")
    @Produces("image/*")
    @GenerateLink(rel = LINK_REL_GET_ICON)
    @ApiOperation(value = "Get icon by stack id",
                  notes = "This operation can be performed only by authorized user",
                  response = byte[].class)
    @ApiResponses({@ApiResponse(code = 200, message = "The response contains requested image entity"),
                   @ApiResponse(code = 403, message = "The user does not have access to get image entity"),
                   @ApiResponse(code = 500, message = "Internal server error occurred")})
    public Response getIcon(@ApiParam("The stack id") @PathParam("id") final String id)
            throws NotFoundException, ServerException, BadRequestException {
        StackImpl stack = stackDao.getById(id);

        if (stack == null) {
            throw new NotFoundException("Stack with id '" + id + "' was not found.");
        }

        StackIcon image = stack.getStackIcon();

        if (image == null) {
            throw new NotFoundException("Image for stack with id '" + id + "' was not found.");
        }
        return Response.ok(image.getData(), image.getMediaType()).build();
    }

    @POST
    @Path("/{id}/icon")
    @Consumes(MULTIPART_FORM_DATA)
    @Produces(TEXT_PLAIN)
    @GenerateLink(rel = LINK_REL_UPLOAD_ICON)
    @ApiOperation(value = "Upload icon for required stack",
                  notes = "This operation can be performed only by authorized stack owner")
    @ApiResponses({@ApiResponse(code = 200, message = "Image was successfully uploaded"),
                   @ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"),
                   @ApiResponse(code = 403, message = "The user does not have access upload image for stack with required id"),
                   @ApiResponse(code = 404, message = "The stack doesn't exist"),
                   @ApiResponse(code = 500, message = "Internal server error occurred")})
    public Response uploadIcon(@ApiParam("The image for stack")
                               final Iterator<FileItem> formData,
                               @ApiParam("The stack id")
                               @PathParam("id")
                               final String id)
            throws NotFoundException, ServerException, BadRequestException, ForbiddenException, ConflictException {
        if (formData.hasNext()) {
            FileItem fileItem = formData.next();
            StackIcon stackIcon = new StackIcon(fileItem.getName(), fileItem.getContentType(), fileItem.get());

            StackImpl stack = stackDao.getById(id);

            stack.setStackIcon(stackIcon);
            stackDao.update(stack);
        }
        return Response.ok().build();
    }

    @DELETE
    @Path("/{id}/icon")
    @GenerateLink(rel = LINK_REL_DELETE_ICON)
    @ApiOperation(value = "Delete icon for required stack",
                  notes = "This operation can be performed only by authorized stack owner")
    @ApiResponses({@ApiResponse(code = 204, message = "Icon was successfully removed"),
                   @ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"),
                   @ApiResponse(code = 403, message = "The user does not have access upload image for stack with required id"),
                   @ApiResponse(code = 404, message = "The stack or icon doesn't exist"),
                   @ApiResponse(code = 409, message = "Conflict error occurred during stack update" +
                                                      "(e.g. Stack with such name already exists)"),
                   @ApiResponse(code = 500, message = "Internal server error occurred")})
    public void removeIcon(@ApiParam("The stack Id") @PathParam("id") final String id)
            throws NotFoundException, ServerException, ConflictException, ForbiddenException, BadRequestException {
        StackImpl stack = stackDao.getById(id);
        stack.setStackIcon(null);
        stackDao.update(stack);
    }

    private StackDto asStackDto(StackImpl stack) {
        final UriBuilder builder = getServiceContext().getServiceUriBuilder();

        List<Link> links = new ArrayList<>();
        final Link removeLink = LinksHelper.createLink("DELETE",
                                                       builder.clone()
                                                              .path(getClass(), "removeStack")
                                                              .build(stack.getId())
                                                              .toString(),
                                                       LINK_REL_REMOVE_STACK);
        final Link getLink = LinksHelper.createLink("GET",
                                                    builder.clone()
                                                           .path(getClass(), "getStack")
                                                           .build(stack.getId())
                                                           .toString(),
                                                    APPLICATION_JSON,
                                                    LINK_REL_GET_STACK_BY_ID);
        links.add(removeLink);
        links.add(getLink);

        StackIcon stackIcon = stack.getStackIcon();
        if (stackIcon != null) {
            Link deleteIcon = LinksHelper.createLink("DELETE",
                                                     builder.clone()
                                                            .path(getClass(), "removeIcon")
                                                            .build(stack.getId())
                                                            .toString(),
                                                     stackIcon.getMediaType(),
                                                     LINK_REL_DELETE_ICON);
            Link getIconLink = LinksHelper.createLink("GET",
                                                      builder.clone()
                                                             .path(getClass(), "getIcon")
                                                             .build(stack.getId())
                                                             .toString(),
                                                      stackIcon.getMediaType(),
                                                      LINK_REL_GET_ICON);
            links.add(deleteIcon);
            links.add(getIconLink);
        }
        return asDto(stack).withLinks(links);
    }
}
